import 'dart:convert';
import 'dart:io';
import "package:args/args.dart";
import 'package:args/command_runner.dart';
import "package:dart_dash_otp/dart_dash_otp.dart";
import 'package:dart_dash_otp/src/otp.dart';

const defaultSavingPath = "key.json";
void main(List<String> arguments) {
  final cmd = CommandRunner(
      "dotp", "A cli-app for generating one-time passwords using 2FA.");
  final parser = cmd.argParser;
  parser.addOption(
    "type",
    abbr: "t",
    help: "Choose the varifying type.",
    allowed: ["hotp", "totp"],
    allowedHelp: {
      "totp": "Using Time-Based One-Time Password Algorithm",
      "hotp":
          "Using HMAC-Based One-Time Password Algorithm(Not currently supported)"
    },
    defaultsTo: "totp",
  );
  parser.addOption("name",
      abbr: "n",
      help:
          "Create a new key and set its name. The key will not be saved if this option is not set.",
      // valueHelp: "Tag for the key",
      aliases: ["new"]);

  parser.addOption("secret",
      abbr: "s", help: "Set the secret of the key.", aliases: ["secrets"]);

  parser.addOption("file",
      abbr: "f",
      help:
          "Specify the file to save the data(defalt to $defaultSavingPath if not set).",
      defaultsTo: defaultSavingPath);

  parser.addOption("digits",
      abbr: "d",
      help: "Specify the digit of the password.",
      valueHelp: "number",
      defaultsTo: "6",
      aliases: ["digit"]);

  parser.addOption("algorithm",
      abbr: "a",
      help: "Specify the algorithm of encryption.",
      allowed: ["sha1,sha256,sha384,sha512"],
      defaultsTo: "sha1");

  parser.addOption("interval",
      abbr: "i", defaultsTo: "30", help: "For TOTP way.");

  parser.addOption("counter",
      abbr: "c", defaultsTo: "0", help: "For HOTP way.");

  parser.addFlag("view",
      abbr: 'v',
      help: "View the passwords.",
      defaultsTo: false,
      negatable: false);
  parser.addFlag("list",
      abbr: 'l',
      help: 'List the passwords. The same as "--view".',
      defaultsTo: false,
      negatable: false);

  ArgResults res;
  try {
    res = cmd.parse(arguments);
  } catch (error) {
    if (error is UsageException) {
      print(error.message);
    } else {
      print("Error");
    }
    print('Run "dotp help" for information about usage.');
    return;
  }

  if (res.flag("help")) {
    cmd.printUsage();
  }
  var name = res.option("name");
  bool shouldSave = name != null;
  var secret = res.option("secret");

  if (shouldSave && secret == null) {
    while (secret == null) {
      print("Input secret:");
      secret = stdin.readLineSync()?.trim();
    }
  }
  if (secret != null) {
    int? digits = int.tryParse(res.option("digits") ?? "6") ?? 6;
    OTPAlgorithm algorithm = getAlgorithm(res.option("algorithm"));

    OTP key;
    if (res.option("type")?.trim().toLowerCase() == "hopt" ||
        res.option("counter") != null) {
      key = HOTP(
          secret: secret,
          digits: digits,
          algorithm: algorithm,
          counter: int.tryParse(res.option("counter") ?? "0") ?? 0);
    } else {
      key = TOTP(
          secret: secret,
          digits: digits,
          algorithm: algorithm,
          interval: int.tryParse(res.option("interval") ?? "30") ?? 30);
    }

    var path = res.option("file") ?? defaultSavingPath;
    if (shouldSave) {
      var pswd = genPassword(key);
      if (pswd == null) {
        print("Failed to generate password. The secret may be incorrect.");
        return;
      }
      print("name: $name");
      print("password: $pswd");
      switch (saveOTP(
        name,
        key,
        path,
      )) {
        case 0:
          print("Succeed in writing to file $path.");
          break;
        case 1:
          print("Cannot find file $path.");
          break;
        case 2:
          print("Failed to read file $path.");
          break;
        default:
          print("Error in saving the data.");
      }
    } else {
      var pswd = genPassword(key);
      if (pswd == null) {
        print("Failed to generate password. The secret may be incorrect.");
        return;
      }
      print("password: $pswd");
    }
  }

  if (res.arguments.isEmpty || res.flag("view") || res.flag("list")) {
    listPasswords(res.option("file") ?? defaultSavingPath);
  }
}

int saveOTP(String name, OTP otp, String path) {
  String data = "{}";
  var file = findFile(path);

  if (file == null) {
    return 1;
  }

  try {
    data = file.readAsStringSync();
  } catch (e) {
    return 2;
  }

  var keys = json.decode(data);
  if (keys == null) {
    return -1;
  }
  keys as Map<String, dynamic>;
  keys.addAll({
    name: {
      "secret": otp.secret,
      "type": otp.type.name,
      "digits": otp.digits,
      "algorithm": otp.algorithm.name,
    }
  });
  if (otp.type == OTPType.TOTP) {
    keys[name]["interval"] = (otp as TOTP).interval;
  }
  if (otp.type == OTPType.HOTP) {
    keys[name]["counter"] = (otp as HOTP).counter;
  }
  var encoder = JsonEncoder.withIndent("  ");
  file.writeAsStringSync(encoder.convert(keys), mode: FileMode.write);
  return 0;
}

OTPAlgorithm getAlgorithm(String? algorithm) {
  switch (algorithm?.toLowerCase()) {
    case "sha1":
      return OTPAlgorithm.SHA1;
    case "sha256":
      return OTPAlgorithm.SHA256;
    case "sha384":
      return OTPAlgorithm.SHA384;
    case "sh512":
      return OTPAlgorithm.SHA512;
    default:
      return OTPAlgorithm.SHA1;
  }
}

String? genPassword(OTP otp) {
  try {
    if (otp.type == OTPType.HOTP) {
      return (otp as HOTP).at(counter: otp.counter);
    }
    return (otp as TOTP).now();
  } catch (e) {
    return null;
  }
}

void listPasswords(String path) {
  Map data = {};
  var file = findFile(path);
  if (file == null) {
    print("Cannot find file $path.");
    return;
  }
  try {
    data = json.decode(file.readAsStringSync())!;
  } catch (e) {
    print("Failed to parse the file.");
    return;
  }
  print('List the passwords from "${file.path}":');

  data.forEach((name, otpData) {
    String secret = otpData["secret"] ?? otpData["secrets"] ?? "";
    if (secret == "") {
      print("Cannot find the secret of $name");
      return;
    }
    int digits = otpData["digits"] ?? otpData["digit"] ?? 6;
    OTP otp;
    if (otpData["type"].toString().trim().toLowerCase() == "hotp") {
      otp = HOTP(
          secret: secret,
          digits: digits,
          algorithm: getAlgorithm(otpData["algorithm"]),
          counter: otpData["counter"] ?? 0);
      data[name]!["counter"] += 1; // Attention!
    } else {
      otp = TOTP(
          secret: secret,
          digits: digits,
          algorithm: getAlgorithm(otpData["algorithm"]),
          interval: otpData["interval"] ?? 30);
    }
    print("[$name]: ${genPassword(otp) ?? 'Failed to generate password.'}");
  });
  var encoder = JsonEncoder.withIndent("  ");
  file.writeAsStringSync(encoder.convert(data), mode: FileMode.write);
  return;
}

File? findFile(String pathOrName) {
  var paths = Platform.environment["Path"]!.split(";");
  paths.addAll(["..", "."]);
  for (var p in paths) {
    if (p.isEmpty) continue;
    var f = File("$p${Platform.pathSeparator}$pathOrName");
    if (f.existsSync()) {
      return f.absolute;
    }
  }
  return null;
}
